SpringBoot 定时任务
参考资料 廖雪峰的官方网站 使用Scheduler
什么是 Spring Scheduled
在很多应用程序中,经常需要执行定时任务。例如,每天或每月给用户发送账户汇总报表,定期检查并发送系统状态报告,等等。
定时任务在使用线程池一节中已经讲到了,Java标准库本身就提供了定时执行任务的功能。在 Spring 中,使用定时任务更简单,不需要手写线程池相关代码,只需要两个注解即可。
缺点:只适合处理简单的计划任务,不能处理分布式计划任务。在计划任务数量太多的时候,可能出现阻塞,崩溃,延迟启动等问题。 优势:是 Spring 框架提供的计划任务,开发简单,执行效率比较高。且
Scheduled 定时任务是 Spring3.0版本之后自带的一个定时任务,如果要单独使用需要导入依赖
<!-- scheduled所属资源为spring-context-support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
如果是在 SpringBoot 使用则无需额外的依赖,可以直接在 AppConfig 中加上 @EnableScheduling
就开启了定时任务的支持
基本使用
首先在启动类上加入 @EnableScheduling
注解
@SpringBootApplication
@EnableScheduling
public class AppStarter {
public static void main(String[] args) {
SpringApplication.run(AppStarter.class, args);
}
}
接下来,可以直接在一个 Bean 中编写一个无参数方法,然后加上 @Scheduled
注解:
@Component
public class TaskService {
final Logger logger = LoggerFactory.getLogger(getClass());
@Scheduled(initialDelay = 60_000, fixedRate = 60_000)
public void checkSystemStatusEveryMinute() {
logger.info("Start check system status...");
}
}
上述注解指定了启动延迟60秒,并以60秒的间隔执行任务。现在,我们直接运行应用程序,就可以在控制台看到定时任务打印的日志:
2020-06-03 18:47:32 INFO [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...
2020-06-03 18:48:32 INFO [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...
2020-06-03 18:49:32 INFO [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...
上面除了可以使用 fixedRate 还可以使用 FixedDelay
FixedRate 是指任务总是以固定时间间隔触发,不管任务执行多长时间
而 FixedDelay 是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:
因此,使用 ScheduledThreadPool 时,我们要根据需要选择执行一次、FixedRate 执行还是 FixedDelay 执行。
定时任务的配置放到配置文件
可以把定时任务的配置放到配置文件中,例如 task.properties
task.checkDiskSpace=30000
这样就可以随时修改配置文件而无需动代码。但是在代码中,我们需要用 fixedDelayString 取代 fixedDelay:
@Component
public class TaskService {
...
@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:30000}")
public void checkDiskSpaceEveryMinute() {
logger.info("Start check disk space...");
}
}
注意到上述代码的注解参数 fixedDelayString 是一个属性占位符,并配有默认值 30000,Spring 在处理 @Scheduled
注解时,如果遇到 String,会根据占位符自动用配置项替换,这样就可以灵活地修改定时任务的配置。
此外,fixedDelayString 还可以使用更易读的 Duration,例如:
@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:PT2M30S}")
以字符串 PT2M30S 表示的 Duration 就是2分30秒(Duration 百度)
使用 Cron 任务
还有一类定时任务,它不是简单的重复执行,而是按时间触发,我们把这类任务称为 Cron 任务,例如:
- 每天凌晨2:15执行报表任务;
- 每个工作日12:00执行特定任务;
- ……
Cron源自 Unix/Linux 系统自带的 crond 守护进程,以一个简洁的表达式定义任务触发时间。在 Spring 中,也可以使用 Cron 表达式来执行 Cron 任务,在 Spring 中,它的格式是:
秒 分 小时 天 月份 星期 年
年是可以忽略的,通常不写。每天凌晨 2:15 执行的 Cron 表达式就是:
0 15 2 * * *
每个工作日 12:00 执行的 Cron 表达式就是:
0 0 12 * * MON-FRI
每个月1号,2号,3号和10号12:00执行的 Cron 表达式就是:
0 0 12 1-3,10 * *
在 Spring 中,我们定义一个每天凌晨2:15执行的任务:
@Component
public class TaskService {
...
@Scheduled(cron = "${task.report:0 15 2 * * *}")
public void cronDailyReport() {
logger.info("Start daily report task...");
}
}
Cron 任务同样可以使用属性占位符,这样修改起来更加方便。
Cron 表达式还可以表达每10分钟执行,例如:
0 */10 * * * *
这样,在每个小时的 0:00,10:00,20:00,30:00,40:00,50:00 均会执行任务,实际上它可以取代 fixedRate 类型的定时任务。
让任务并行执行
默认情况下,@Scheduled
任务都在 Spring 创建的大小为1 的默认线程池中执行,可以通过在加了 @Scheduled
注解的方法里加上下面这段代码来验证。
logger.info("Current Thread : {}", Thread.currentThread().getName());
加上上面这段代码的定时任务,每次运行都会输出:
Current Thread : scheduling-1
要让定时任务并行执行有下面两种方式
自定义线程池并行执行
如果需要自定义线程池执行话只需要新加一个实现 SchedulingConfigurer 接口的 configureTasks 的类即可,这个类需要加上 @Configuration
注解。
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
private final int POOL_SIZE = 10;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
threadPoolTaskScheduler.initialize();
scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}
这样再次执行上面的任务输出当前线程的名字会改变。
使用异步来并行执行
如果想要代码并行执行的话,还可以通过 @EnableAsync
和 @Async
这两个注解实现
@Component
@EnableAsync
public class AsyncScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
*/
//@Async
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行程序输出如下,reportCurrentTimeWithFixedDelay()
方法会每5秒执行一次,因为 @Scheduled
任务都在 Spring 创建的大小为1 的默认线程池中执行。
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:23
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:28
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:33
把 reportCurrentTimeWithFixedDelay()
方法上的 @Async 注解取消注释后输出如下,reportCurrentTimeWithFixedDelay()
方法会每 2 秒执行一次。
Current Thread : task-1
Fixed Delay Task : The time is now 14:27:32
Current Thread : task-2
Fixed Delay Task : The time is now 14:27:34
Current Thread : task-3
Fixed Delay Task : The time is now 14:27:36
创建一个 scheduled task 示例
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* fixedRate:固定速率执行。每5秒执行一次。
*/
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
log.info("Current Thread : {}", Thread.currentThread().getName());
log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
}
/**
* fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
*/
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* initialDelay:初始延迟。任务的第一次执行将延迟5秒,然后将以5秒的固定间隔执行。
*/
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
log.info("Fixed Rate Task with Initial Delay : The time is now {}", dateFormat.format(new Date()));
}
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
}